package com.google.code.hibernate.rest.method;
import static com.google.code.hibernate.rest.internal.InternalPreconditions.checkNotNull;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Represents a URI template. An URI template is a URI-like String that
 * contained variables marked of in braces (<code>{</code>, <code>}</code>),
 * which can be expanded to produce a URI.
 * <p>
 * 
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @see <a href="http://bitworking.org/projects/URI-Templates/">URI
 *      Templates</a>
 */
class URITemplate {

	/** Captures URI template variable names. */
	private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");

	/** Replaces template variables in the URI template. */
	private static final String VALUE_REGEX = "(.*)";

	private final List<String> variableNames;

	private final Pattern matchPattern;

	private final String uriTemplate;

	/**
	 * Construct a new {@link URITemplate} with the given URI String.
	 * 
	 * @param uriTemplate
	 *            the URI template string
	 */
	public URITemplate(String uriTemplate) {
		checkNotNull(uriTemplate, "uriTemplate");
		Parser parser = new Parser(uriTemplate);
		this.uriTemplate = uriTemplate;
		this.variableNames = parser.getVariableNames();
		this.matchPattern = parser.getMatchPattern();
	}


	/**
	 * Indicate whether the given URI matches this template.
	 * 
	 * @param uri
	 *            the URI to match to
	 * @return <code>true</code> if it matches; <code>false</code> otherwise
	 */
	public boolean matches(String uri) {
		if (uri == null) {
			return false;
		}
		Matcher matcher = this.matchPattern.matcher(uri);
		return matcher.matches();
	}

	/**
	 * Match the given URI to a map of variable values. Keys in the returned map
	 * are variable names, values are variable values, as occurred in the given
	 * URI.
	 * <p>
	 * Example:
	 * 
	 * <pre class="code">
	 * UriTemplate template = new UriTemplate(&quot;http://example.com/hotels/{hotel}/bookings/{booking}&quot;);
	 * System.out.println(template.match(&quot;http://example.com/hotels/1/bookings/42&quot;));
	 * </pre>
	 * 
	 * will print: <blockquote><code>{hotel=1, booking=42}</code></blockquote>
	 * 
	 * @param uri
	 *            the URI to match to
	 * @return a map of variable values
	 */
	public Map<String, String> match(String uri) {
		checkNotNull(uri, "uri");
		Map<String, String> result = new LinkedHashMap<String, String>(this.variableNames.size());
		Matcher matcher = this.matchPattern.matcher(uri);
		if (matcher.find()) {
			for (int i = 1; i <= matcher.groupCount(); i++) {
				String name = this.variableNames.get(i - 1);
				String value = matcher.group(i);
				result.put(name, value);
			}
		}
		return result;
	}

	@Override
	public String toString() {
		return this.uriTemplate;
	}

	/**
	 * Static inner class to parse uri template strings into a matching regular
	 * expression.
	 */
	private static class Parser {

		private final List<String> variableNames = new LinkedList<String>();

		private final StringBuilder patternBuilder = new StringBuilder();

		private Parser(String uriTemplate) {
			checkNotNull(uriTemplate, "uriTemplate");
			Matcher m = NAMES_PATTERN.matcher(uriTemplate);
			int end = 0;
			while (m.find()) {
				this.patternBuilder.append(quote(uriTemplate, end, m.start()));
				this.patternBuilder.append(VALUE_REGEX);
				this.variableNames.add(m.group(1));
				end = m.end();
			}
			this.patternBuilder.append(quote(uriTemplate, end, uriTemplate.length()));
			int lastIdx = this.patternBuilder.length() - 1;
			if (lastIdx >= 0 && this.patternBuilder.charAt(lastIdx) == '/') {
				this.patternBuilder.deleteCharAt(lastIdx);
			}
		}

		private String quote(String fullPath, int start, int end) {
			if (start == end) {
				return "";
			}
			return Pattern.quote(fullPath.substring(start, end));
		}

		private List<String> getVariableNames() {
			return Collections.unmodifiableList(this.variableNames);
		}

		private Pattern getMatchPattern() {
			return Pattern.compile(this.patternBuilder.toString());
		}
	}

}
